﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.IO;
using System.Net;
using System.Web;

namespace FusionDotNet
{
    /// <summary>
    /// Base implementation of the command object. This class defines the structure for making calls against the Google REST api.
    /// </summary>
    /// <typeparam name="TResult"></typeparam>
    public abstract class Command<TResult> where TResult : CommandResult, new()
    {
        /// <summary>
        /// Dictionary used to store the result of an async operation associated with a particular IAsyncResult until EndExecute is called.
        /// </summary>
        private Dictionary<IAsyncResult, TResult> m_AsyncResults;

        /// <summary>
        /// Gets/Sets the <see cref="AuthenticationToken"/> used with the Command.
        /// </summary>
        /// <remarks>
        /// When this property is set the command will inject the correct headers into the GET/POST to authenticate the request against the Google API.
        /// </remarks>
        public AuthenticationToken AuthenticationToken { get; set; }

        #region Async Support

        /// <summary>
        /// Indicates that the Command is still executing.
        /// </summary>
        public bool IsBusy { get; protected set; }

        /// <summary>
        /// Fired when a ExecuteAsync or BeginExecute completes.
        /// </summary>
        public event ExecuteCompletedEventHandler ExecuteCompletedEvent;

        /// <summary>
        /// Delegate for the ExecuteCompletedEvent.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public delegate void ExecuteCompletedEventHandler(object sender, ExecuteCompletedEventArgs e);

        /// <summary>
        /// Used to provide typed results to asynchronous calls.
        /// </summary>
        public class ExecuteCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs
        {
            /// <summary>
            /// Construct a new ExecuteCompletedEventArgs
            /// </summary>
            /// <param name="error"></param>
            /// <param name="cancelled"></param>
            /// <param name="userState"></param>
            public ExecuteCompletedEventArgs(Exception error, bool cancelled, object userState)
                : base(error, cancelled, userState)
            {
            }

            /// <summary>
            /// Typed implementation of CommandResult.
            /// </summary>
            public TResult Result { get; set; }
        }

        /// <summary>
        /// Execute this command asynchronously.
        /// </summary>
        public virtual void ExecuteAsync()
        {
            ExecuteCommandAsync();
        }

        /// <summary>
        /// When overridden in a derived class allows the command object to specify the asynchronous execution behaviour.
        /// </summary>
        protected abstract void ExecuteCommandAsync();
        
        /// <summary>
        /// Called on completion of an asynchronous operation. In the event of a SecurityException or a WebException of type 'Bad Request (400)' this method invokes CreateExceptionResult. 
        /// </summary>
        /// <param name="result"></param>
        protected virtual void ExecuteComplete(IAsyncResult result)
        {
            RequestState state = (RequestState)result.AsyncState;
            HttpWebRequest request = state.WebRequest;
            TResult commandResult = null;

            try
            {
                state.WebResponse = (HttpWebResponse)request.EndGetResponse(result);
                CheckForLoginRedirect(request, state.WebResponse);
                using (Stream responseStream = state.WebResponse.GetResponseStream())
                {
                    StreamReader reader = new StreamReader(responseStream);
                    commandResult = CreateResult(reader);
                }
            }
            catch (System.Security.SecurityException ex)
            {
                commandResult = CreateExceptionResult(ex);
            }
            catch (WebException ex)
            {
                HttpWebResponse response = ex.Response as HttpWebResponse;
                if (response != null && response.StatusCode == HttpStatusCode.BadRequest)
                {
                    commandResult = CreateExceptionResult(ExceptionBuilder.BuildException<InvalidOperationException>(response.StatusDescription, ex));
                }
                else
                {
                    throw;
                }
            }

            ExecuteCompletedEvent(this, new ExecuteCompletedEventArgs(null, false, null) { Result = commandResult });
            IsBusy = false;
        }
        #endregion

        /// <summary>
        /// Synchronously executes the command.
        /// </summary>
        /// <returns></returns>
        public virtual TResult Execute()
        {
            try
            {
                using (Stream resultStream = ExecuteCommand())
                {
                    StreamReader reader = new StreamReader(resultStream);
                    return CreateResult(reader);
                }
            }
            catch (System.Security.SecurityException ex)
            {
                return CreateExceptionResult(ex);
            }
            catch (WebException ex)
            {
                HttpWebResponse response = ex.Response as HttpWebResponse;
                if(response != null)
                {
                    if(response.StatusCode == HttpStatusCode.BadRequest)
                    {
                        return CreateExceptionResult(ExceptionBuilder.BuildException<InvalidOperationException>(response.StatusDescription,ex));
                    }
                }
                throw;
            }
        }

        /// <summary>
        /// When overridden in a derived class converts the information provided by <paramref name="reader"/> into a CommandResult.
        /// </summary>
        /// <param name="reader">StreamReader containing the results of the command execution.</param>
        /// <returns></returns>
        internal abstract TResult CreateResult(StreamReader reader);
        
        /// <summary>
        /// When overridden in a derived class creates a CommandResult based on <paramref name="exception"/>.
        /// </summary>
        /// <param name="exception">The System.Exception to base the CommandResult on.</param>
        /// <returns></returns>
        public virtual TResult CreateExceptionResult(Exception exception)
        {
            TResult result = new TResult();
            result.Success = false;
            result.AddException(exception);
            return result;
        }

        /// <summary>
        /// Returns the default System.Text.Encoding. The default is UTF8 as required by Google.
        /// </summary>
        /// <returns></returns>
        protected virtual Encoding GetContentEncoding()
        {
            return new UTF8Encoding();
        }

        /// <summary>
        /// Returns the base Uri. The default is 'http://tables.googlelabs.com/api/query'
        /// </summary>
        /// <returns></returns>
        protected virtual string GetRequestUri()
        {
            return "http://tables.googlelabs.com/api/query";
        }

        /// <summary>
        /// Creates a HttpWebRequest for Uri as constructed in GetRequestUri. If AuthenticationToken is set it will add the appropriate headers to the request.
        /// </summary>
        /// <returns></returns>
        protected virtual HttpWebRequest CreateWebRequest()
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(GetRequestUri());
            
            if(AuthenticationToken != null)
            {
                request.Headers.Add("Authorization: GoogleLogin auth=" + AuthenticationToken.Token);
            }
            
            return request;
        }

        /// <summary>
        /// When overridden in a derived class executes the current command and returns the resultant Stream.
        /// </summary>
        /// <returns></returns>
        protected abstract Stream ExecuteCommand();

        /// <summary>
        /// Tests to see if the response has been redirected to the Google Login page. 
        /// This occurs when you attempt to access a private resource anonymously. 
        /// </summary>
        /// <exception cref="System.Security.SecurityException">
        /// Thrown when the response has been redirected to the ServiceLogin page.
        /// </exception>
        protected void CheckForLoginRedirect(HttpWebRequest request, HttpWebResponse response)
        {
            if (request.RequestUri != response.ResponseUri) //We have been redirected...
            {
                if (response.ResponseUri.AbsolutePath == "/accounts/ServiceLogin") //We have been redirected to the login screen.
                {
                    string message = String.Format("The GET command resulted in a redirect to the Google Fusion login page. Verify that the requested table supports public access or that you have provided the correct authentication token.");
                    throw ExceptionBuilder.BuildException<System.Security.SecurityException>(message);
                }
            }
        }

        #region IAsyncResult Implementation
        
        /// <summary>
        /// Execute the command asynchronously and return the IAsyncResult. Use this method if you want low level control over the asynchronous operation. If not use ExecuteAsync and implement the ExecuteCompletedEvent event handler.
        /// </summary>
        /// <remarks>
        /// This function will not invoke the call-back if EndExecute is invoked immediately after the call to BeginExecute.
        /// </remarks>
        /// <param name="callback">Delegate to the function to invoke on completion of the call.</param>
        /// <param name="state">User State.</param>
        /// <returns>IAsyncResult of the operation</returns>
        public IAsyncResult BeginExecute(AsyncCallback callback, object state)
        {

            FusionAsyncResult result = new FusionAsyncResult(state);

            TrackAsyncResult(result);

            this.ExecuteCompletedEvent += (o, s) =>
            {
                m_AsyncResults[result] = s.Result;
                result.IsCompleted = true;
                if (!result.CompletedSynchronously) //The client immediately called EndExecute.
                {
                    callback.Invoke(result);
                }
                ((System.Threading.EventWaitHandle)result.AsyncWaitHandle).Set();
            };

            ExecuteCommandAsync();

            return result;
        }

        /// <summary>
        /// Completes an asynchronous operation. If this method is called before the async operation is completed the thread will block until it returns.
        /// </summary>
        /// <param name="result"></param>
        /// <returns></returns>
        public TResult EndExecute(IAsyncResult result)
        {
            if (m_AsyncResults.ContainsKey(result))
            {
                if (m_AsyncResults[result] == null) //We have not received a response yet.
                {
                    ((FusionAsyncResult)result).CompletedSynchronously = true;
                    result.AsyncWaitHandle.WaitOne(); //Block the thread and wait for a call back.\
                }

                TResult commandResult = m_AsyncResults[result];

                //Stop tracking this IAsyncResult. 
                m_AsyncResults.Remove(result);

                return commandResult;
            }
            else
            {
                throw new InvalidOperationException("The provided IAsyncResult is invalid for the state of the command. Ensure that EndExecute has not already been called for this IAsyncResult.");
            }
        }

        /// <summary>
        /// Adds the AsyncResult to the tracking dictionary. This call initialises m_AsynResults if required.
        /// </summary>
        /// <param name="result"></param>
        private void TrackAsyncResult(FusionAsyncResult result)
        {
            if (m_AsyncResults == null)
            {
                m_AsyncResults = new Dictionary<IAsyncResult, TResult>();
            }

            m_AsyncResults.Add(result, null);
        }


        #endregion
    }

}
